package com.tastespot.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tastespot.constants.RedisConstants;
import com.tastespot.model.dto.Result;
import com.tastespot.model.dto.UserDTO;
import com.tastespot.model.entity.SeckillVoucher;
import com.tastespot.model.entity.VoucherOrder;
import com.tastespot.mapper.VoucherOrderMapper;
import com.tastespot.service.ISeckillVoucherService;
import com.tastespot.service.IVoucherOrderService;
import com.tastespot.utils.RedisIdWorker;
import com.tastespot.utils.UserHolder;
import com.tastespot.utils.lock.Lock;
import com.tastespot.utils.lock.impl.SimpleRedisLock;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.aop.framework.AopContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.Objects;

import static com.tastespot.constants.RedisConstants.SECKILL_VOUCHER_ORDER;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
    @Resource
    private ISeckillVoucherService seckillVoucherService;

    @Resource
    private RedissonClient redissonClient;
    
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private RedisIdWorker redisIdWorker;

    @Resource
    private RabbitTemplate rabbitTemplate;


    private static final DefaultRedisScript<Long> SECKILL_SCRIPT;

    static {
        SECKILL_SCRIPT = new DefaultRedisScript<>();
        SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
        SECKILL_SCRIPT.setResultType(Long.class);
    }


    /**
     * 将用户是否有权限下单逻辑放到lua脚本
     */
    @Override
    public Result secKillVoucher(Long voucherId) {
        Long userId = UserHolder.getUser().getId();
        // 1、执行Lua脚本，判断用户是否具有秒杀资格
        Long result = null;
        try {
            result = stringRedisTemplate.execute(
                    SECKILL_SCRIPT,
                    Collections.emptyList(),
                    voucherId.toString(),
                    userId.toString()
            );
        } catch (Exception e) {
            log.error("Lua脚本执行失败");
            throw new RuntimeException(e);
        }
        if (result != null && !result.equals(0L)) {
            // result为1表示库存不足，result为2表示用户已下单
            int r = result.intValue();
            return Result.fail(r == 2 ? "不能重复下单" : "库存不足");
        }
        // 2. 发送消息到MQ, 然后消费者端就可以直接用 @Transactional   无需拿到proxy对象
        VoucherOrder voucherOrder = new VoucherOrder();
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        voucherOrder.setUserId(userId);
        voucherOrder.setVoucherId(voucherId);

        // 3. 推送到 MQ
        rabbitTemplate.convertAndSend(
                "seckill-exchange",    // 事先在 RabbitMQ 控制台/配置里声明好的交换机
                "seckill.voucher",     // 路由键
                voucherOrder                  // 消息体
        );
        return Result.ok();
    }
//    @Override
//    public Result secKillVoucher(Long voucherId) {
//        UserDTO user = UserHolder.getUser();
//        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
//        LocalDateTime now = LocalDateTime.now();
//        if(now.isAfter(seckillVoucher.getEndTime()) || now.isBefore(seckillVoucher.getBeginTime())){
//            return Result.fail("秒杀尚未开始或者已经过期");
//        }
//        int stock = seckillVoucher.getStock();
//        if(stock < 1){
//            return Result.fail("库存不足");
//        }
//        Long userId = user.getId();
//        // note 6. 即使同一个用户发起多个请求，每个请求对应的 ThreadLocal 中存储的对象也是不同的。Long的自动装箱在-128~127范围内会缓存对象，
//        //  但超出该范围时每次会创建新对象，也不能用user.getId().toString() 底层调用new String方法
//        // intern方法： 将这个字符串对象尝试放入串池(stringTable)，如果有则并不会放入，如果没有则放入串池； 会把串池中的对象返回
//        // intern() 会返回字符串常量池中的唯一引用，确保相同值的字符串是同一对象。
//        // note 7. 先加锁，再加事务（分析文档见飞书）
//        // synchronized (userId.toString().intern()){.....}
//        // 使用redisson分布式锁
//        RLock lock = redissonClient.getLock(RedisConstants.LOCK_ORDER_KEY + userId);
//        boolean isLock = lock.tryLock();
//        if(!isLock){
//            return Result.fail("不允许重复下单");
//        }
//        try{
//            // Spring AOP 默认使用 JDK 动态代理，该代理机制基于【接口】进行代理, 所以获取的是接口代理对象
//            IVoucherOrderService proxy = (IVoucherOrderService)AopContext.currentProxy();
//            return proxy.createVoucherOrder(voucherId, userId);
//        }finally {
//            lock.unlock();
//        }
//    }


    /**
     * 一人一单问题解决, 无法用乐观锁比较 用户买了0单与 1单情况
     * 乐观锁只适用于 【更新数据】比如之前的
     * oolean success = seckillVoucherService.update()
        .setSql("stock= stock -1")     |  此处为版本号法      |
        .eq("voucher_id", voucherId).eq("stock",voucher.getStock()).update();
     */
    @Transactional
    public Result createVoucherOrder(long voucherId, long userId){
        // 1. 判断用户是否下过单
        int count = this.count(new LambdaQueryWrapper<VoucherOrder>()
                .eq(VoucherOrder::getUserId, userId));
        if(count > 0){
            return Result.fail("用户已经下过单");
        }
        // 2. 扣减库存
        // 方案二：乐观锁（具体分析见飞书文档）
        LambdaUpdateWrapper<SeckillVoucher> updateWrapper = new LambdaUpdateWrapper<>();
        // set 方法会将 stock 字段直接设置为 stock - 1 的计算结果，而不是在数据库层面执行 stock = stock - 1 的操作。
        // note 5 惊天大bug: updateWrapper.set(SeckillVoucher::getStock, stock - 1)
        updateWrapper.setSql("stock = stock - 1")
                .eq(SeckillVoucher::getVoucherId, voucherId)
                .gt(SeckillVoucher::getStock, 0);
        boolean success = seckillVoucherService.update(updateWrapper);
        if(!success){
            return Result.fail("库存扣减失败");
        }

        // 3. 创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        long orderId = redisIdWorker.nextId(SECKILL_VOUCHER_ORDER);
        voucherOrder.setId(orderId);
        voucherOrder.setVoucherId(voucherId);
        voucherOrder.setUserId(userId);
        save(voucherOrder);
        return Result.ok(orderId);
    }
}
